﻿using System;
using System.Collections.Generic;
using System.Net;
using System.Net.NetworkInformation;
using System.Text;
using System.Threading;

namespace Common.Utilities.NetworkUtilities
{
    /// <summary>
    /// 
    /// </summary>
    /// <param name="host"></param>
    public delegate void PingDelegate(PingUtil host);

    /// <summary>
    /// 
    /// </summary>
    /// <param name="status"></param>
    public delegate void PingStatusUpdate(string status);

    /// <summary>
    /// uses .net framework 2 Ping class to determine responsiveness of remote server
    /// </summary>
    public class PingUtil
    {
        #region Events
        public event PingDelegate OnPing;
        public event PingStatusUpdate OnPingStatusUpdate;
        #endregion

        #region const
        public static readonly bool DEFALUT_FRAGMENT = false;
        public static readonly int DEFALUT_PINGS_BEFORE_DEAD = 10;
        public static readonly int DEFAULT_BUFFER_SIZE = 32;
        public static readonly int DEFAULT_TIMEOUT = 2000;
        public static readonly int DEFAULT_TTL = 32;
        public static readonly int DEFAULT_PING_INTERVAL = 1000;
        #endregion

        #region fields
        object _syncStatistics = new object();
        object _syncOptions = new object();
        object _syncControl = new object();
        byte[] _buffer = new byte[DEFAULT_BUFFER_SIZE];
        System.Timers.Timer _timer = new System.Timers.Timer();
        Ping _pinger = new Ping();
        PingOptions _pingerOptions = new PingOptions(DEFAULT_TTL, DEFALUT_FRAGMENT);
        #endregion

        #region props

        private IPAddress _hostIP;
        /// <summary></summary>
        public IPAddress HostIP
        {
            get { return _hostIP; }
            set { _hostIP = value; }
        }

        private string _hostName = string.Empty;
        /// <summary></summary>
        public string HostName
        {
            get { return _hostName; }
            set { _hostName = value; }
        }

        private int _sentPackets;
        /// <summary></summary>
        public int SentPackets
        {
            get
            {
                lock (_syncStatistics)
                    return _sentPackets;
            }
        }

        private int _recivedPackets;
        /// <summary></summary>
        public int RecivedPackets
        {
            get
            {
                lock (_syncStatistics)
                    return _recivedPackets;
            }
        }

        /// <summary></summary>
        public float RecivedPacketsPercents
        {
            get
            {
                lock (_syncStatistics)
                    return _recivedPackets / _sentPackets * 100;
            }
        }

        private int _lostPackets;
        /// <summary></summary>
        public int LostPackets
        {
            get
            {
                lock (_syncStatistics)
                    return _lostPackets;
            }
        }

        /// <summary></summary>
        public float LostPacketsPercent
        {
            get
            {
                lock (_syncStatistics)
                    return (float)_lostPackets / _sentPackets * 100;
            }
        }

        private long _currentResponseTime;
        /// <summary></summary>
        public long CurrentResponseTime
        {
            get
            {
                lock (_syncStatistics)
                    return _currentResponseTime;
            }
        }

        private long _totalResponseTime = 0;
        /// <summary></summary>
        public float AvargeResponseTime
        {
            get
            {
                lock (_syncStatistics)
                    return _recivedPackets != 0 ? (float)_totalResponseTime / _recivedPackets : 0;
            }
        }

        private int _continousPacketLost = 0;

        private bool _isRunning;
        /// <summary></summary>
        public bool IsRunning
        {
            get
            {
                lock (_syncControl)
                    return _isRunning;
            }

            set
            {
                if (value)
                    Start();
                else
                    Stop();
            }
        }

        private HostStatus _status = HostStatus.Unknown;
        /// <summary></summary>
        public HostStatus Status
        {
            get
            {
                lock (_syncControl)
                {
                    lock (_syncStatistics)
                        return _isRunning ? _status : HostStatus.Unknown;
                }
            }
        }

        private DateTime _statusReachedAt = DateTime.Now;
        /// <summary></summary>
        public TimeSpan StatusDuration
        {
            get
            {
                lock (_syncStatistics)
                    return DateTime.Now.Subtract(_statusReachedAt);
            }
        }

        private DateTime _startTime = DateTime.Now;
        /// <summary></summary>
        public TimeSpan TestDuration
        {
            get
            {
                lock (_syncControl)
                {
                    lock (_syncStatistics)
                        return _isRunning ? DateTime.Now.Subtract(_startTime) : new TimeSpan(0);
                }
            }
        }

        private bool _dontFragment;
        /// <summary></summary>
        public bool DontFragment
        {
            get
            {
                lock (_syncOptions)
                    return _dontFragment;
            }
            set
            {
                lock (_syncOptions)
                {
                    if (value != _dontFragment)
                    {
                        _dontFragment = value;
                        _pingerOptions.DontFragment = value;
                    }
                }
            }
        }

        private int _ttl = DEFAULT_TTL;
        /// <summary></summary>
        public int TTL
        {
            get
            {
                lock (_syncOptions)
                    return _ttl;
            }
            set
            {
                lock (_syncOptions)
                {
                    if (value > 0 && value != _ttl)
                    {
                        _ttl = value;
                        _pingerOptions.Ttl = value;
                    }
                }
            }
        }

        private int _timeout = DEFAULT_TIMEOUT;
        /// <summary>
        /// 
        /// </summary>
        public int Timeout
        {
            get
            {
                lock (_syncOptions)
                    return _timeout;
            }
            set
            {
                lock (_syncOptions)
                    _timeout = value;
            }
        }

        private int _bufferSize = DEFAULT_BUFFER_SIZE;
        /// <summary></summary>
        public int BufferSize
        {
            get
            {
                lock (_syncOptions)
                    return _bufferSize;
            }
            set
            {
                lock (_syncOptions)
                {
                    if (value > 0)
                    {
                        _bufferSize = value;
                        _buffer = new byte[value];
                    }
                }
            }
        }

        private int _pingInterval = DEFAULT_PING_INTERVAL;
        /// <summary></summary>
        public int PingInterval
        {
            get
            {
                lock (_syncOptions)
                    return _pingInterval;
            }
            set
            {
                lock (_syncOptions)
                    _pingInterval = value;
            }
        }

        private int _pingsBeforeDead = DEFALUT_PINGS_BEFORE_DEAD;
        /// <summary>
        /// 
        /// </summary>
        public int PingsBeforeDead
        {
            get
            {
                lock (_syncOptions)
                    return _pingsBeforeDead;
            }
            set
            {
                lock (_syncOptions)
                    _pingsBeforeDead = value;
            }
        }
        #endregion

        #region public

        /// <summary>
        /// 
        /// </summary>
        public void Start()
        {
            Monitor.Enter(_syncControl);
            if (!_isRunning)
            {
                _isRunning = true;

                lock (_syncStatistics)
                    // save test startting time 
                    _startTime = DateTime.Now;

                Monitor.Exit(_syncControl);

                // run first ping
                Thread t = new Thread(new ThreadStart(Pinger));
                t.Start();

                // log command
                this.LogPingStatus("Pinging started");

                return;
            }
            Monitor.Exit(_syncControl);
        }

        /// <summary>
        /// 
        /// </summary>
        public void Stop()
        {
            lock (_syncControl)
            {
                // log command
                if (_isRunning)
                    this.LogPingStatus("Ping stopped");

                _continousPacketLost = 0;

                _isRunning = false;
            }
        }

        #endregion

        #region private
        private void IncLost()
        {
            lock (_syncStatistics)
            {
                _sentPackets++;
                _lostPackets++;

                lock (_syncOptions)
                {
                    // enough packets has been lost so we can assume that the host is dead 
                    if (++_continousPacketLost > _pingsBeforeDead && _status != HostStatus.Dead)
                    {
                        _statusReachedAt = DateTime.Now;
                        this.LogPingStatus("Host died!");

                        _status = HostStatus.Dead;
                    }
                }
            }
        }

        private void IncRecived(long time)
        {
            lock (_syncStatistics)
            {
                _sentPackets++;
                _recivedPackets++;

                _totalResponseTime += time;
                _currentResponseTime = time;

                // restarts counter
                _continousPacketLost = 0;

                if (_status != HostStatus.Alive)
                {
                    _statusReachedAt = DateTime.Now;
                    this.LogPingStatus("Host is alive!");

                    _status = HostStatus.Alive;
                }
            }
        }

        private void RaisePing()
        {
            if (OnPing != null)
                OnPing(this);
        }

        private void LogPingStatus(string status)
        {
            if (this.OnPingStatusUpdate != null)
                this.OnPingStatusUpdate(status);
        }

        private void Pinger()
        {
            lock (_syncControl)
            {
                PingReply reply;
                lock (_syncOptions)
                    // send ping message
                    reply = _pinger.Send(_hostIP, _timeout, _buffer, _pingerOptions);

                switch (reply.Status)
                {
                    case IPStatus.BadDestination:
                    case IPStatus.BadHeader:
                    case IPStatus.BadOption:
                    case IPStatus.BadRoute:
                    case IPStatus.UnrecognizedNextHeader:
                    case IPStatus.PacketTooBig:
                    case IPStatus.ParameterProblem:
                        // wrong message format
                        IncLost();
                        break;

                    case IPStatus.DestinationScopeMismatch:
                    case IPStatus.Unknown:
                    case IPStatus.HardwareError:
                    case IPStatus.IcmpError:
                    case IPStatus.NoResources:
                    case IPStatus.SourceQuench:
                        // error
                        IncLost();
                        break;

                    case IPStatus.DestinationHostUnreachable:
                    case IPStatus.DestinationNetworkUnreachable:
                    case IPStatus.DestinationPortUnreachable:
                    case IPStatus.DestinationProhibited:
                    case IPStatus.DestinationUnreachable:
                        // unreachability of the remote host
                        IncLost();
                        break;

                    case IPStatus.TimeExceeded:
                    case IPStatus.TimedOut:
                    case IPStatus.TtlExpired:
                    case IPStatus.TtlReassemblyTimeExceeded:
                        // time outs
                        IncLost();
                        break;

                    case IPStatus.Success:
                        // success
                        IncRecived(reply.RoundtripTime);
                        break;

                    default:
                        // something went wrong
                        IncLost();
                        break;
                }

                // still runnning?
                if (_isRunning)
                {
                    lock (_syncOptions)
                        // new interval if it was change
                        _timer.Interval = _pingInterval;

                    // ping another
                    _timer.Start();
                }
            }

            // raise event
            RaisePing();
        }
        #endregion
    }
}
